/*********************************************
 * Student    : Teodoro Cipresso
 * Course     : CS286
 * Professor  : Vidya Setlur
 * Assignment : Term Project (BlueTanks)
 *********************************************/
package edu.sjsu.cs286.teodoro.cipresso.bluetooth;

import java.io.IOException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.DeviceClass;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.DiscoveryListener;
import javax.bluetooth.LocalDevice;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.ServiceRecord;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.io.StreamConnectionNotifier;
import javax.microedition.lcdui.Display;

import edu.sjsu.cs286.teodoro.cipresso.wizard.BlueTankGameContainer;

/**
 * This class implements management of Bluetooth communication between the
 * devices running the game client. The anatomy of a Blueooth application has
 * five parts: stack initialization, device management, device discovery,
 * service discovery, and communication.
 * 
 * @author Teodoro Cipresso, based on a tutorial with code snippets from
 *         http://developers.sun.com/techtopics/mobility/midp/articles/bluetooth2/
 */
public class BlueTankConnectionManager implements DiscoveryListener,
		IBlueTankConnectionManager {

	private LocalDevice localDevice;

	private String localDeviceAddress;

	private String localDeviceName;

	private DiscoveryAgent discoveryAgent;

	private Hashtable remoteBtDevices;

	private Vector remoteBtServices;

	private boolean connected = false;

	private boolean server = false;

	private BlueTankGameContainer container;

	private StreamConnection streamConn;

	private int thisSrchTranID = -1;

	private BlueTankMessageHandler btMessageHandler;

	private BlueTankGameStatusScreen statScr;

	/**
	 * Default constructor. Initial screen is game welcome screen.
	 */
	public BlueTankConnectionManager(BlueTankGameContainer parent) {
		//
		container = parent;
		statScr = container.getStatScreen();
		//
		statScr.clearStatusMessages();
		statScr.setTitle("BlueTank Startup");
		statScr.addStatusMessage("Welcome to BlueTanks!");
		statScr.addStatusMessage("Choose 'Host Game' or 'Join Game'");
		statScr.addStatusMessage("from the Options menu.");
		statScr.addConnectSoftKeys();
		//
		Display.getDisplay(container.getMidlet()).setCurrent(statScr);
		//
	} // BlueTankConnectionManager

	/**
	 * Goes through the stages of device discovery, service search and
	 * establishing a connection with a peer player as server. This is done on a
	 * seperate thread to avoid deadlock with commandAction handlers.
	 */
	public void doBtServerInit() {
		new Thread(new BtServerInit()).start();
	} // doBtServerInit

	/**
	 * Goes through the stages of device discovery, service search and
	 * establishing a connection with a peer player as server
	 */
	private class BtServerInit implements Runnable {
		/**
		 */
		public void run() {
			//
			statScr.clearStatusMessages();
			//
			closeBtConnection();
			connected = false;
			server = true;
			//
			if (container != null)
				initLocalBluetoothDevice();
			//
			if (localDevice != null)
				btDeviceDiscover();
			//
			initBtServer();
			//
			if (!connected) {
				server = false;
			} else {
				statScr.removeConnectSoftKeys();
			}
		} // run()
	} // BtServerInit

	/**
	 * Goes through the stages of device discovery, service search and
	 * establishing a connection with a peer player as client
	 */
	public void doBtClientInit() {
		new Thread(new BtClientInit()).start();
	} // doBtClientInit

	/**
	 * Goes through the stages of device discovery, service search and
	 * establishing a connection with a peer player as client. This is done on a
	 * seperate thread to avoid deadlock with commandAction handlers.
	 */
	private class BtClientInit implements Runnable {
		/**
		 */
		public void run() {
			//
			statScr.clearStatusMessages();
			//
			closeBtConnection();
			connected = false;
			//
			if (container != null)
				initLocalBluetoothDevice();
			//
			if (localDevice != null)
				btDeviceDiscover();
			//
			if (remoteBtDevices.size() > 0)
				btServiceDiscover();
			//
			initBtClient();
			// keep trying
			if (!connected) {
				run();
			} else {
				statScr.removeConnectSoftKeys();
				server = false;
			}
			//
		} // run()
	} // BtClientInit

	/**
	 * LocalDevice depends on the javax.bluetooth.DeviceClass class to retrieve
	 * the device's type and the kinds of services it offers.
	 */
	private void initLocalBluetoothDevice() {
		container.getStatScreen().addStatusMessage("Initializing Bluetooth...");
		//
		try {
			// retrieve the local Bluetooth device object
			localDevice = LocalDevice.getLocalDevice();
			// retrieve the Bluetooth address of the local device
			localDeviceAddress = localDevice.getBluetoothAddress();
			// Servers set the discoverable mode to GIAC
			localDevice.setDiscoverable(DiscoveryAgent.GIAC);
			// retrieve the name of the local Bluetooth device
			localDeviceName = localDevice.getFriendlyName();
		} catch (Exception e) {
			e.printStackTrace();
		}
	} // initLocalBluetoothDevice()

	/**
	 * A Bluetooth device can use a DiscoveryAgent object to obtain a list of
	 * accessible devices.
	 */
	private void btDeviceDiscover() {
		container.getStatScreen().addStatusMessage(
				"Detecting Bluetooth Devices...");
		// retrieve the discovery agent
		discoveryAgent = localDevice.getDiscoveryAgent();
		// place the device in inquiry mode
		try {
			//
			remoteBtDevices = new Hashtable();
			remoteBtServices = new Vector();
			// add preknown devices to the list
			RemoteDevice[] devices = discoveryAgent
					.retrieveDevices(DiscoveryAgent.PREKNOWN);
			if (devices != null && devices.length > 0) {
				for (int n = 0; n < devices.length; n++) {
					if (!remoteBtDevices.containsKey(devices[n]
							.getBluetoothAddress())) {
						remoteBtDevices.put(devices[n].getBluetoothAddress(),
								devices[n]);
					} // if
				} // for
			} // if
			// add cached devices to the list
			devices = discoveryAgent.retrieveDevices(DiscoveryAgent.CACHED);
			if (devices != null && devices.length > 0) {
				for (int n = 0; n < devices.length; n++) {
					if (!remoteBtDevices.containsKey(devices[n]
							.getBluetoothAddress())) {
						remoteBtDevices.put(devices[n].getBluetoothAddress(),
								devices[n]);
					} // if
				} // for
			} // if
			// start inquiry to find non-cached, non-preknown devices
			if (discoveryAgent.startInquiry(DiscoveryAgent.GIAC, this)) {
				// Wait until all the devices are found
				synchronized (this) {
					try {
						this.wait();
					} catch (Exception e) {
					}
				}
			} else {
				System.out
						.println("BlueTankConnectionManager#deviceDiscovery(): couldn't start inquiry");

			}

		} catch (BluetoothStateException e) {
			e.printStackTrace();
		}
	}// btDeviceDiscover()

	/**
	 * Once nearby devices have been discovered, the client can search for
	 * services of interest. We limit the scope of the search for the BlueTank
	 * service where interested.
	 */
	private void btServiceDiscover() {
		//
		statScr.addStatusMessage("Searching for Game Host...");
		//
		try {
			//
			remoteBtServices = new Vector();
			// we short circuit the search when we find one device that is
			// running the game service
			Enumeration e = remoteBtDevices.elements();
			while (e.hasMoreElements() && remoteBtServices.size() == 0) {
				// get device to search
				RemoteDevice rd = (RemoteDevice) e.nextElement();
				// search services on device using agent
				thisSrchTranID = discoveryAgent.searchServices(null, uuids, rd,
						this);
				// Wait until all the services are found
				synchronized (this) {
					try {
						this.wait();
					} catch (Exception e1) {
					}
				}
			} // while
		} catch (BluetoothStateException e) {
			e.printStackTrace();
		}
	} // btServiceDiscover()

	/**
	 * This method is called when this device will be the Master in the
	 * bluetooth network. This corresponds to "Host Game" at the main menu.
	 */
	private void initBtServer() {
		//
		try {
			// Create a server connection (a notifier)
			StreamConnectionNotifier scn = (StreamConnectionNotifier) Connector
					.open(SERVER_CONNECTION_URL, Connector.READ_WRITE, true);
			//
			statScr.addStatusMessage("Game Host Ready, Waiting...");
			// Accept a new client connection
			streamConn = scn.acceptAndOpen();
			// start listener
			btMessageHandler = new BlueTankMessageHandler(container);
			new Thread(btMessageHandler).start();
			//
			connected = true;
			//
		} catch (Exception e) {
			statScr.addStatusMessage("initBtServer:" + e.getMessage());
		}
	} // initBtServer()

	/**
	 * This method is called when this device will be the Slave in the bluetooth
	 * network. This corresponds to "Join Game" at the main menu.
	 */
	private void initBtClient() {
		//
		try {
			// Given a service of interest, get its service record
			ServiceRecord sr = (ServiceRecord) remoteBtServices.elementAt(0);
			// Then get the service's connection URL
			String connURL = sr.getConnectionURL(
					ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false);
			// 
			statScr.addStatusMessage("Connecting to Game Host ("
					+ sr.getHostDevice().getFriendlyName(true) + ")...");
			// Open connection
			streamConn = (StreamConnection) Connector.open(connURL);
			btMessageHandler = new BlueTankMessageHandler(container);
			// test bi-directional communication with server
			boolean result = btMessageHandler.peformDataRoundtripTest();
			if (result) {
				statScr.addStatusMessage("Data Roundtrip OK");
			} else {
				statScr.addStatusMessage("Data Roundtrip Failed!");
			}
			Thread.sleep(500);
			// stream connection listener
			new Thread(btMessageHandler).start();
			//
			connected = true;
			//
		} catch (Exception e) {
			statScr.addStatusMessage("initBtClient:" + e.getMessage());
		}
	} // initBtClient()

	/**
	 * Called when a device was found during an inquiry.
	 */
	public void deviceDiscovered(RemoteDevice btDevice, DeviceClass cod) {
		System.out.println("deviceDiscovered()");
		if (!remoteBtDevices.containsKey(btDevice.getBluetoothAddress()))
			remoteBtDevices.put(btDevice.getBluetoothAddress(), btDevice);
	} // deviceDiscovered()

	/**
	 * Called when a device discovery transaction is completed.
	 */
	public void inquiryCompleted(int discType) {
		System.out.println("inquiryCompleted()");
		synchronized (this) {
			try {
				this.notifyAll();
			} catch (Exception e) {
			}
		}
	} // inquiryCompleted()

	/**
	 * Called when service(s) are found during a service search. =
	 */
	public void servicesDiscovered(int transID, ServiceRecord[] service) {
		System.out.println("servicesDiscovered");
		for (int i = 0; i < service.length; i++) {
			remoteBtServices.addElement(service[i]);
		}
	} // servicesDiscovered()

	/**
	 * The following method is called when a service search is completed.
	 */
	public void serviceSearchCompleted(int transID, int respCode) {
		System.out.println("serviceSearchCompleted()");
		//
		synchronized (this) {
			try {
				this.notifyAll();
			} catch (Exception e) {
			}
		}
	} // serviceSearchCompleted()

	/**
	 * This method cancels any device/service search and closes any open
	 * sockets.
	 */
	public void closeBtConnection() {
		try {
			// stop inquiry process (if running)
			if (discoveryAgent != null) {
				discoveryAgent.cancelInquiry(this);
				discoveryAgent.cancelServiceSearch(thisSrchTranID);
			}
			// close I/O streams opened on StreamConnection
			if (btMessageHandler != null) {
				btMessageHandler.closeInputOutputStreams();
			}
			// close StreamConnection
			if (streamConn != null) {
				streamConn.close();
			}
			//
			connected = false;
		} catch (IOException e) {

		}
	} // closeBtConnection()

	/**
	 * get the connection with peer
	 */
	public StreamConnection getStreamConn() {
		return streamConn;
	} // getStreamConn()

	/**
	 * Accessor for bluetooth device
	 */
	public LocalDevice getLocalDevice() {
		return localDevice;
	} // getLocalDevice()

	/**
	 * Accessor for bluetooh device address
	 */
	public String getLocalDeviceAddress() {
		return localDeviceAddress;
	} // getLocalDeviceAddress()

	/**
	 * Accessor for bluetooth device name
	 */
	public String getLocalDeviceName() {
		return localDeviceName;
	} // getLocalDeviceName()

	/**
	 * Get the message handler instance to use to send messages between game
	 * clients
	 */
	public BlueTankMessageHandler getBtMessageHandler() {
		return btMessageHandler;
	} // getBtMessageHandler()

	/**
	 * Return whether a connection with peer has been established
	 */
	public boolean isConnected() {
		return connected;
	} // isConnected()

	/**
	 * Indicate whether this device is the Master
	 */
	public boolean isServer() {
		return server;
	} // isServer()
}
